/*
 * Su-yun Information Technology, Co.,Ltd CONFIDENTIAL
 *
 * (C) Copyright Su-yun Information Technology, Co.,Ltd. 2021 All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains the property of
 * Su-yun Information Technology, Co.,Ltd. The intellectual property herein
 * are protected by trade secret, patent law or copyright law. Dissemination
 * of this information or reproduction of this material is strictly forbidden
 * unless prior written permission is obtained from Su-yun Information Technology.
 *
 * https://suyunplat.com/
 *
 */
package com.xtiim.battery.discharge.protocal.manager;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.xtiim.battery.discharge.common.concurrent.pool.GlobalExecutor;
import com.xtiim.battery.discharge.modules.htm.model.HostInfo;
import com.xtiim.battery.discharge.modules.htm.service.HostInfoService;
import com.xtiim.battery.discharge.modules.htm.service.impl.HostInfoServiceImpl;
import com.xtiim.battery.discharge.serial.DefaultPortEventLister;
import com.xtiim.battery.discharge.util.SerialPortUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import purejavacomm.SerialPort;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Manage host and port.
 */
@Slf4j
public class HostManager {

    /**
     * < host, host info>
     */
    private Map<String, HostInfo> hostMap = Maps.newConcurrentMap();

    /**
     * <host, portName>
     */
    private Map<String, String> hostPortMap = Maps.newConcurrentMap();

    /**
     * <host, serialPortInfo>>
     */
    private Map<String, SerialPortInfo> hostSerialPortMap = Maps.newConcurrentMap();

    /**
     * <portName, host>
     */
    private Map<String, String> portHostMap = Maps.newConcurrentMap();

    /**
     * <portName, serialPortInfo>
     * COM6 -> portInfo
     */
    private Map<String, SerialPortInfo> portSerialPortInfoMap = Maps.newConcurrentMap();

    /**
     * <host, host comm status>
     */
    private Map<String, HostCommStatus> hostCommStatusMap = Maps.newConcurrentMap();

    private List<SerialPortInfo> serialPortInfoList = Lists.newArrayList();

    private GlobalExecutor globalExecutor = GlobalExecutor.getInstance();

    private FailedPortChecker failedPortChecker = new FailedPortChecker();

    private static final int sendCommandCount = 5;

    private HostInfoService hostInfoService = HostInfoServiceImpl.getInstance();

    /**
     * <host, command count>
     */
    private Map<String, AtomicInteger> sendCommandCountMap = Maps.newConcurrentMap();

    private static HostManager instance = new HostManager();

    private HostManager() {
    }

    public static HostManager getInstance() {
        return instance;
    }

    public void init() {
        List<HostInfo> hostInfos = hostInfoService.getAllOpenedHostInfos();
        if (CollectionUtil.isEmpty(hostInfos)) {
            return;
        }
        // host -> hostInfo
        Map<String, HostInfo> hostInfoMap = hostInfos.stream().collect(Collectors.toMap(HostInfo::getNumber, Function.identity()));
        for (String host : hostInfoMap.keySet()) {
            addHost(host, hostInfoMap.get(host));
        }

        globalExecutor.scheduleFailedPortCheck(failedPortChecker);
    }

    /**
     * Add host into management
     *
     * @param host
     */
    public void addHost(String host, HostInfo hostInfo) {
        // first judge if it is in management
        if (hostMap.keySet().contains(host)) {
            return;
        }

        SerialPortInfo serialPortInfo = null;
        String portName = hostInfo.getSerialPort();

        if (serialPortInfo == null) {
            serialPortInfo = openPort(portName);
        }

        if (serialPortInfo != null) {
            addSerialPort(host, serialPortInfo);
        } else {
            log.error("[Serial port open failed] host: {}, port: {}", host, portName);
            hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_FAILED);
        }

        hostMap.put(host, hostInfo);
    }

    /**
     * Remove host from management
     *
     * @param host
     */
    public void removeHost(String host) {
        if (!hostMap.keySet().contains(host)) {
            return;
        }

        hostMap.remove(host);
        hostCommStatusMap.remove(host);

        String port = hostPortMap.get(host);
        hostPortMap.remove(host);

        SerialPortInfo serialPortInfo = hostSerialPortMap.get(host);
        if (serialPortInfo != null) {
            hostSerialPortMap.remove(host);
        }

        if (StrUtil.isNotBlank(port)) {
            portSerialPortInfoMap.remove(port);
        }
    }

    /**
     * After serial port is opened, add serial port into management.
     *
     * @param host
     * @param serialPortInfo
     */
    private void addSerialPort(String host, SerialPortInfo serialPortInfo) {
        hostSerialPortMap.put(host, serialPortInfo);
        portSerialPortInfoMap.put(serialPortInfo.getSerialPort().getName(), serialPortInfo);
        // portName is COM6, but serialPort.getName() is //./COM6
        hostPortMap.put(host, serialPortInfo.getSerialPort().getName());
        portHostMap.put(serialPortInfo.getSerialPort().getName(), host);

        hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_NORMAL);
    }

    /**
     * For port which failed to open, check it and try to open.
     */
    private class FailedPortChecker implements Runnable {

        @Override
        public void run() {
            for (String host : hostMap.keySet()) {
                try {
                    SerialPortInfo serialPortInfo = hostSerialPortMap.get(host);
                    if (serialPortInfo == null) {
                        HostInfo hostInfo = hostMap.get(host);

                        if (serialPortInfo == null) {
                            serialPortInfo = openPort(hostInfo.getSerialPort());
                        }
                        if (serialPortInfo != null) {
                            addSerialPort(host, serialPortInfo);
                        }
                    }
                } catch (Throwable e) {
                    log.error("Error occurs in failed port check. error: {}", ExceptionUtils.getStackTrace(e));
                }
            }
        }
    }

    /**
     * Get all host names
     *
     * @return
     */
    public List<String> getAllHosts() {
        return new ArrayList<>(hostMap.keySet());
    }

    /**
     * Get host by port
     *
     * @param port
     * @return
     */
    public String getHostByPort(String port) {
        String host = portHostMap.get(port);
        return host;
    }

    /**
     * Close serial port while port communication failed
     * before reopen afterwards
     *
     * @param port phy port name
     */
    public void closeSerialPort(String port) {
        String host = portHostMap.get(port);
        hostPortMap.remove(host);
        hostSerialPortMap.remove(host);
        hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_FAILED);

        portHostMap.remove(port);
        SerialPortInfo serialPortInfo = portSerialPortInfoMap.get(port);
        if (serialPortInfo != null) {
            SerialPortUtil.closePort(serialPortInfo.getSerialPort());
            serialPortInfoList.remove(serialPortInfo);
        }
        portSerialPortInfoMap.remove(port);
    }

    /**
     * Get serial port based on host name
     *
     * @param host
     * @return
     */
    public SerialPort getSerialPortByHost(String host) {
        SerialPortInfo serialPortInfo = hostSerialPortMap.get(host);
        return serialPortInfo != null ? serialPortInfo.getSerialPort() : null;
    }

    /**
     * Get serial port based on phy port name
     *
     * @param phyPortName
     * @return
     */
    public SerialPort getSerialPortByPort(String phyPortName) {
        SerialPortInfo serialPortInfo = portSerialPortInfoMap.get(phyPortName);
        return serialPortInfo != null ? serialPortInfo.getSerialPort() : null;
    }

    /**
     * Get host info
     *
     * @param host
     * @return
     */
    public HostInfo getHost(String host) {
        return hostMap.get(host);
    }

    /**
     * Increase host communication timeout count.
     *
     * @param port
     */
    public synchronized void incrTimeoutCount(String port) {
        SerialPortInfo serialPortInfo = portSerialPortInfoMap.get(port);
        if (serialPortInfo != null) {
            serialPortInfo.incrTimeoutCount();
            if (serialPortInfo.getTimeoutCount() >= 3) {
                setCommunicationFailed(port);
            }
        }
    }

    /**
     * Clear host communication timeout count.
     *
     * @param port
     */
    public synchronized void clearTimeoutCount(String port) {
        SerialPortInfo serialPortInfo = portSerialPortInfoMap.get(port);
        if (serialPortInfo != null) {
            serialPortInfo.clearTimeoutCount();
        }
    }

    /**
     * Set host communication failed
     *
     * @param port
     */
    public void setCommunicationFailed(String port) {
        String host = portHostMap.get(port);
        hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_FAILED);
    }

    /**
     * Set host communication normal
     *
     * @param port
     */
    public void setCommunicationNormal(String port) {
        String host = portHostMap.get(port);
        hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_NORMAL);
    }

    /**
     * Restore host communication status
     *
     * @param port
     */
    public void restoreHostCommStatus(String port) {
        clearTimeoutCount(port);
        setCommunicationNormal(port);
    }

    /**
     * Get host communication status
     *
     * @param host
     * @return
     */
    public HostCommStatus getHostCommStatus(String host) {
        return hostCommStatusMap.get(host);
    }

    /**
     * Open serial port
     *
     * @param portName simple port name
     * @return
     */
    private synchronized SerialPortInfo openPort(String portName) {
        for (SerialPortInfo info : serialPortInfoList) {
            if (info.getSimplePortName().equals(portName)) {
                return info;
            }
        }

        SerialPortInfo serialPortInfo = null;
        SerialPort serialPort = SerialPortUtil.openPort(portName, 2400, 8,
                SerialPort.PARITY_NONE, SerialPort.STOPBITS_1);
        if (serialPort != null) {
            SerialPortUtil.addListener(serialPort, new DefaultPortEventLister(serialPort));
            serialPortInfo = new SerialPortInfo(portName, serialPort, 0);
            serialPortInfoList.add(serialPortInfo);
            log.info("[Serial Port Opened] {}", portName);
        }
        return serialPortInfo;
    }

    /**
     * count send command num
     *
     * @param host
     */
    public void countSendCommandNum(String host) {
        AtomicInteger hostSendCommandCount = sendCommandCountMap.get(host);
        if (hostSendCommandCount == null) { //increase efficiency
            hostSendCommandCount = sendCommandCountMap.computeIfAbsent(host, e -> new AtomicInteger());
        }

        int count = 0;
        if (hostSendCommandCount.get() < 10) { //count at most 10
            count = hostSendCommandCount.incrementAndGet();
        } else {
            count = hostSendCommandCount.get();
        }

        if (count > sendCommandCount) {
            String serialPort = hostPortMap.get(host);
            hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_FAILED);
            //todo: websocket push
        }
    }

    /**
     * clear send command num
     *
     * @param host
     */
    public void clearSendCommandNum(String host) {
        sendCommandCountMap.remove(host);
        hostCommStatusMap.put(host, HostCommStatus.COMMUNICATION_NORMAL);
    }

    /**
     * Serial port info
     */
    @Data
    @AllArgsConstructor
    public class SerialPortInfo {
        private String simplePortName;
        private SerialPort serialPort;
        private int timeoutCount;

        public void incrTimeoutCount() {
            timeoutCount++;
        }

        public void clearTimeoutCount() {
            timeoutCount = 0;
        }
    }
}
